/****************************************************************************
**
** This file is part of the KD Chart library.
**
** SPDX-FileCopyrightText: 2001 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
**
** SPDX-License-Identifier: MIT
**
****************************************************************************/

#include "KDChartCartesianDiagramDataCompressor_p.h"

#include <QAbstractItemModel>
#include <QtDebug>

#include "KDChartAbstractCartesianDiagram.h"

#include <KDABLibFakes>

using namespace KDChart;
using namespace std;

CartesianDiagramDataCompressor::CartesianDiagramDataCompressor(QObject *parent)
    : QObject(parent)
{
    calculateSampleStepWidth();
    m_data.resize(0);
}

static bool contains(const CartesianDiagramDataCompressor::AggregatedDataValueAttributes &aggregated,
                     const DataValueAttributes &attributes)
{
    CartesianDiagramDataCompressor::AggregatedDataValueAttributes::const_iterator it = aggregated.constBegin();
    for (; it != aggregated.constEnd(); ++it) {
        if (it.value() == attributes) {
            return true;
        }
    }
    return false;
}

CartesianDiagramDataCompressor::AggregatedDataValueAttributes CartesianDiagramDataCompressor::aggregatedAttrs(
    const AbstractDiagram *diagram,
    const QModelIndex &index,
    const CachePosition &position) const
{
    // return cached attrs, if any
    DataValueAttributesCache::const_iterator i = m_dataValueAttributesCache.constFind(position);
    if (i != m_dataValueAttributesCache.constEnd()) {
        return i.value();
    }

    // aggregate attributes from all indices in the same CachePosition as index
    CartesianDiagramDataCompressor::AggregatedDataValueAttributes aggregated;
    const auto modelList = mapToModel(position);
    for (const QModelIndex &neighborIndex : modelList) {
        DataValueAttributes attrs = diagram->dataValueAttributes(neighborIndex);
        // only store visible and unique attributes
        if (!attrs.isVisible()) {
            continue;
        }
        if (!contains(aggregated, attrs)) {
            aggregated[neighborIndex] = attrs;
        }
    }
    // if none of the attributes had the visible flag set, we just take the one set for the index
    // to avoid returning an empty list (### why not return an empty list?)
    if (aggregated.isEmpty()) {
        aggregated[index] = diagram->dataValueAttributes(index);
    }

    m_dataValueAttributesCache[position] = aggregated;
    return aggregated;
}

bool CartesianDiagramDataCompressor::prepareDataChange(const QModelIndex &parent, bool isRows,
                                                       int *start, int *end)
{
    if (parent != m_rootIndex) {
        return false;
    }
    Q_ASSERT(*start <= *end);

    CachePosition startPos = isRows ? mapToCache(*start, 0) : mapToCache(0, *start);
    CachePosition endPos = isRows ? mapToCache(*end, 0) : mapToCache(0, *end);

    static const CachePosition nullPosition;
    if (startPos == nullPosition) {
        rebuildCache();
        startPos = isRows ? mapToCache(*start, 0) : mapToCache(0, *start);
        endPos = isRows ? mapToCache(*end, 0) : mapToCache(0, *end);
        // The start position still isn't valid,
        // means that no resolution was set yet or we're about to add the first rows
        if (startPos == nullPosition) {
            return false;
        }
    }

    *start = isRows ? startPos.row : startPos.column;
    *end = isRows ? endPos.row : endPos.column;
    return true;
}

void CartesianDiagramDataCompressor::slotRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, true, &start, &end)) {
        return;
    }
    for (int i = 0; i < m_data.size(); ++i) {
        Q_ASSERT(start >= 0 && start <= m_data[i].size());
        m_data[i].insert(start, end - start + 1, DataPoint());
    }
}

void CartesianDiagramDataCompressor::slotRowsInserted(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, true, &start, &end)) {
        return;
    }
    for (int i = 0; i < m_data.size(); ++i) {
        for (int j = start; j < m_data[i].size(); ++j) {
            retrieveModelData(CachePosition(j, i));
        }
    }
}

void CartesianDiagramDataCompressor::slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, false, &start, &end)) {
        return;
    }
    const int rowCount = qMin(m_model ? m_model->rowCount(m_rootIndex) : 0, m_xResolution);
    Q_ASSERT(start >= 0 && start <= m_data.size());
    m_data.insert(start, end - start + 1, QVector<DataPoint>(rowCount));
}

void CartesianDiagramDataCompressor::slotColumnsInserted(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, false, &start, &end)) {
        return;
    }
    for (int i = start; i < m_data.size(); ++i) {
        for (int j = 0; j < m_data[i].size(); ++j) {
            retrieveModelData(CachePosition(j, i));
        }
    }
}

void CartesianDiagramDataCompressor::slotRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, true, &start, &end)) {
        return;
    }
    for (int i = 0; i < m_data.size(); ++i) {
        m_data[i].remove(start, end - start + 1);
    }
}

void CartesianDiagramDataCompressor::slotRowsRemoved(const QModelIndex &parent, int start, int end)
{
    if (parent != m_rootIndex)
        return;
    Q_ASSERT(start <= end);
    Q_UNUSED(end)

    CachePosition startPos = mapToCache(start, 0);
    static const CachePosition nullPosition;
    if (startPos == nullPosition) {
        // Since we should already have rebuilt the cache, it won't help to rebuild it again.
        // Do not Q_ASSERT() though, since the resolution might simply not be set or we might now have 0 rows
        return;
    }

    for (int i = 0; i < m_data.size(); ++i) {
        for (int j = startPos.row; j < m_data[i].size(); ++j) {
            retrieveModelData(CachePosition(j, i));
        }
    }
}

void CartesianDiagramDataCompressor::slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
    if (!prepareDataChange(parent, false, &start, &end)) {
        return;
    }
    m_data.remove(start, end - start + 1);
}

void CartesianDiagramDataCompressor::slotColumnsRemoved(const QModelIndex &parent, int start, int end)
{
    if (parent != m_rootIndex)
        return;
    Q_ASSERT(start <= end);
    Q_UNUSED(end);

    const CachePosition startPos = mapToCache(0, start);

    static const CachePosition nullPosition;
    if (startPos == nullPosition) {
        // Since we should already have rebuilt the cache, it won't help to rebuild it again.
        // Do not Q_ASSERT() though, since the resolution might simply not be set or we might now have 0 columns
        return;
    }

    for (int i = startPos.column; i < m_data.size(); ++i) {
        for (int j = 0; j < m_data[i].size(); ++j) {
            retrieveModelData(CachePosition(j, i));
        }
    }
}

void CartesianDiagramDataCompressor::slotModelHeaderDataChanged(Qt::Orientation orientation, int first, int last)
{
    if (orientation != Qt::Vertical)
        return;

    const int rowCount = m_model->rowCount(m_rootIndex);
    if (rowCount > 0) {
        const QModelIndex firstRow = m_model->index(0, first, m_rootIndex); // checked
        const QModelIndex lastRow = m_model->index(rowCount - 1, last, m_rootIndex); // checked

        slotModelDataChanged(firstRow, lastRow);
    }
}

void CartesianDiagramDataCompressor::slotModelDataChanged(
    const QModelIndex &topLeftIndex,
    const QModelIndex &bottomRightIndex)
{
    if (topLeftIndex.parent() != m_rootIndex)
        return;
    Q_ASSERT(topLeftIndex.isValid());
    Q_ASSERT(bottomRightIndex.isValid());
    Q_ASSERT(topLeftIndex.parent() == bottomRightIndex.parent());
    Q_ASSERT(topLeftIndex.row() <= bottomRightIndex.row());
    Q_ASSERT(topLeftIndex.column() <= bottomRightIndex.column());
    CachePosition topleft = mapToCache(topLeftIndex);
    CachePosition bottomright = mapToCache(bottomRightIndex);
    for (int row = topleft.row; row <= bottomright.row; ++row)
        for (int column = topleft.column; column <= bottomright.column; ++column)
            invalidate(CachePosition(row, column));
}

void CartesianDiagramDataCompressor::slotModelLayoutChanged()
{
    rebuildCache();
    calculateSampleStepWidth();
}

void CartesianDiagramDataCompressor::slotDiagramLayoutChanged(AbstractDiagram *diagramBase)
{
    auto *diagram = qobject_cast<AbstractCartesianDiagram *>(diagramBase);
    Q_ASSERT(diagram);
    if (diagram->datasetDimension() != m_datasetDimension) {
        setDatasetDimension(diagram->datasetDimension());
    }
}

int CartesianDiagramDataCompressor::modelDataColumns() const
{
    Q_ASSERT(m_datasetDimension != 0);
    // only operational if there is a model and a resolution
    if (m_model) {
        const int effectiveDimension = m_datasetDimension == 2 ? 2 : 1;
        const int columns = m_model->columnCount(m_rootIndex) / effectiveDimension;
        Q_ASSERT(columns == m_data.size());
        return columns;
    } else {
        return 0;
    }
}

int CartesianDiagramDataCompressor::modelDataRows() const
{
    // only operational if there is a model, columns, and a resolution
    if (m_model && m_model->columnCount(m_rootIndex) > 0 && m_xResolution > 0) {
        return m_data.isEmpty() ? 0 : m_data.first().size();
    } else {
        return 0;
    }
}

void CartesianDiagramDataCompressor::setModel(QAbstractItemModel *model)
{
    if (model == m_model) {
        return;
    }

    if (m_model != nullptr) {
        disconnect(m_model, &QAbstractItemModel::headerDataChanged,
                   this, &CartesianDiagramDataCompressor::slotModelHeaderDataChanged);
        disconnect(m_model, &QAbstractItemModel::dataChanged,
                   this, &CartesianDiagramDataCompressor::slotModelDataChanged);
        disconnect(m_model, &QAbstractItemModel::layoutChanged,
                   this, &CartesianDiagramDataCompressor::slotModelLayoutChanged);
        disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted,
                   this, &CartesianDiagramDataCompressor::slotRowsAboutToBeInserted);
        disconnect(m_model, &QAbstractItemModel::rowsInserted,
                   this, &CartesianDiagramDataCompressor::slotRowsInserted);
        disconnect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved,
                   this, &CartesianDiagramDataCompressor::slotRowsAboutToBeRemoved);
        disconnect(m_model, &QAbstractItemModel::rowsRemoved,
                   this, &CartesianDiagramDataCompressor::slotRowsRemoved);
        disconnect(m_model, &QAbstractItemModel::columnsAboutToBeInserted,
                   this, &CartesianDiagramDataCompressor::slotColumnsAboutToBeInserted);
        disconnect(m_model, &QAbstractItemModel::columnsInserted,
                   this, &CartesianDiagramDataCompressor::slotColumnsInserted);
        disconnect(m_model, &QAbstractItemModel::columnsRemoved,
                   this, &CartesianDiagramDataCompressor::slotColumnsRemoved);
        disconnect(m_model, &QAbstractItemModel::columnsAboutToBeRemoved,
                   this, &CartesianDiagramDataCompressor::slotColumnsAboutToBeRemoved);
        disconnect(m_model, &QAbstractItemModel::modelReset,
                   this, &CartesianDiagramDataCompressor::rebuildCache);
        m_model = nullptr;
    }

    m_modelCache.setModel(model);

    if (model != nullptr) {
        m_model = model;
        connect(m_model, &QAbstractItemModel::headerDataChanged,
                this, &CartesianDiagramDataCompressor::slotModelHeaderDataChanged);
        connect(m_model, &QAbstractItemModel::dataChanged,
                this, &CartesianDiagramDataCompressor::slotModelDataChanged);
        connect(m_model, &QAbstractItemModel::layoutChanged,
                this, &CartesianDiagramDataCompressor::slotModelLayoutChanged);
        connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted,
                this, &CartesianDiagramDataCompressor::slotRowsAboutToBeInserted);
        connect(m_model, &QAbstractItemModel::rowsInserted,
                this, &CartesianDiagramDataCompressor::slotRowsInserted);
        connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved,
                this, &CartesianDiagramDataCompressor::slotRowsAboutToBeRemoved);
        connect(m_model, &QAbstractItemModel::rowsRemoved,
                this, &CartesianDiagramDataCompressor::slotRowsRemoved);
        connect(m_model, &QAbstractItemModel::columnsAboutToBeInserted,
                this, &CartesianDiagramDataCompressor::slotColumnsAboutToBeInserted);
        connect(m_model, &QAbstractItemModel::columnsInserted,
                this, &CartesianDiagramDataCompressor::slotColumnsInserted);
        connect(m_model, &QAbstractItemModel::columnsRemoved,
                this, &CartesianDiagramDataCompressor::slotColumnsRemoved);
        connect(m_model, &QAbstractItemModel::columnsAboutToBeRemoved,
                this, &CartesianDiagramDataCompressor::slotColumnsAboutToBeRemoved);
        connect(m_model, &QAbstractItemModel::modelReset,
                this, &CartesianDiagramDataCompressor::rebuildCache);
    }
    rebuildCache();
    calculateSampleStepWidth();
}

void CartesianDiagramDataCompressor::setRootIndex(const QModelIndex &root)
{
    if (m_rootIndex != root) {
        Q_ASSERT(root.model() == m_model || !root.isValid());
        m_rootIndex = root;
        m_modelCache.setRootIndex(root);
        rebuildCache();
        calculateSampleStepWidth();
    }
}

void CartesianDiagramDataCompressor::recalcResolution()
{
    setResolution(m_xResolution, m_yResolution);
}

void CartesianDiagramDataCompressor::setResolution(int x, int y)
{
    if (setResolutionInternal(x, y)) {
        rebuildCache();
        calculateSampleStepWidth();
    }
}

bool CartesianDiagramDataCompressor::setResolutionInternal(int x, int y)
{
    const int oldXRes = m_xResolution;
    const int oldYRes = m_yResolution;

    if (m_datasetDimension != 1) {
        // just ignore the X resolution in that case
        m_xResolution = m_model ? m_model->rowCount(m_rootIndex) : 0;
    } else {
        m_xResolution = qMax(0, x);
    }
    m_yResolution = qMax(0, y);

    return m_xResolution != oldXRes || m_yResolution != oldYRes;
}

void CartesianDiagramDataCompressor::clearCache()
{
    for (int column = 0; column < m_data.size(); ++column)
        m_data[column].fill(DataPoint());
}

void CartesianDiagramDataCompressor::rebuildCache()
{
    Q_ASSERT(m_datasetDimension != 0);

    m_data.clear();
    setResolutionInternal(m_xResolution, m_yResolution);
    const int columnDivisor = m_datasetDimension == 2 ? 2 : 1;
    const int columnCount = m_model ? m_model->columnCount(m_rootIndex) / columnDivisor : 0;
    const int rowCount = qMin(m_model ? m_model->rowCount(m_rootIndex) : 0, m_xResolution);
    m_data.resize(columnCount);
    for (int i = 0; i < columnCount; ++i) {
        m_data[i].resize(rowCount);
    }
    // also empty the attrs cache
    m_dataValueAttributesCache.clear();
}

const CartesianDiagramDataCompressor::DataPoint &CartesianDiagramDataCompressor::data(const CachePosition &position) const
{
    static DataPoint nullDataPoint;
    if (!mapsToModelIndex(position)) {
        return nullDataPoint;
    }
    if (!isCached(position)) {
        retrieveModelData(position);
    }
    return m_data[position.column][position.row];
}

QPair<QPointF, QPointF> CartesianDiagramDataCompressor::dataBoundaries() const
{
    const int colCount = modelDataColumns();
    qreal xMin = std::numeric_limits<qreal>::quiet_NaN();
    qreal xMax = std::numeric_limits<qreal>::quiet_NaN();
    qreal yMin = std::numeric_limits<qreal>::quiet_NaN();
    qreal yMax = std::numeric_limits<qreal>::quiet_NaN();

    for (int column = 0; column < colCount; ++column) {
        const DataPointVector &data = m_data[column];
        int row = 0;
        for (DataPointVector::const_iterator it = data.begin(); it != data.end(); ++it, ++row) {
            const DataPoint &p = *it;
            if (!p.index.isValid())
                retrieveModelData(CachePosition(row, column));

            if (ISNAN(p.key) || ISNAN(p.value)) {
                continue;
            }

            if (ISNAN(xMin)) {
                xMin = p.key;
                xMax = p.key;
                yMin = p.value;
                yMax = p.value;
            } else {
                xMin = qMin(xMin, p.key);
                xMax = qMax(xMax, p.key);
                yMin = qMin(yMin, p.value);
                yMax = qMax(yMax, p.value);
            }
        }
    }

    const QPointF bottomLeft(xMin, yMin);
    const QPointF topRight(xMax, yMax);
    return qMakePair(bottomLeft, topRight);
}

void CartesianDiagramDataCompressor::retrieveModelData(const CachePosition &position) const
{
    Q_ASSERT(mapsToModelIndex(position));
    DataPoint result;
    result.hidden = true;

    switch (m_mode) {
    case Precise: {
        const QModelIndexList indexes = mapToModel(position);

        if (m_datasetDimension == 2) {
            Q_ASSERT(indexes.count() == 2);
            const QModelIndex &xIndex = indexes.at(0);
            result.index = xIndex;
            result.key = m_modelCache.data(xIndex);
            result.value = m_modelCache.data(indexes.at(1));
        } else {
            if (indexes.isEmpty()) {
                break;
            }
            result.value = std::numeric_limits<qreal>::quiet_NaN();
            result.key = 0.0;
            for (const QModelIndex &index : indexes) {
                const qreal value = m_modelCache.data(index);
                if (!ISNAN(value)) {
                    result.value = ISNAN(result.value) ? value : result.value + value;
                }
                result.key += index.row();
            }
            result.index = indexes.at(0);
            result.key /= indexes.size();
            result.value /= indexes.size();
        }

        for (const QModelIndex &index : indexes) {
            // the DataPoint point is visible if any of the underlying, aggregated points is visible
            if (m_model->data(index, DataHiddenRole).value<bool>() == false) {
                result.hidden = false;
            }
        }
        break;
    }
    case SamplingSeven:
        break;
    }

    m_data[position.column][position.row] = result;
    Q_ASSERT(isCached(position));
}

CartesianDiagramDataCompressor::CachePosition CartesianDiagramDataCompressor::mapToCache(
    const QModelIndex &index) const
{
    Q_ASSERT(m_datasetDimension != 0);

    static const CachePosition nullPosition;
    if (!index.isValid()) {
        return nullPosition;
    }
    return mapToCache(index.row(), index.column());
}

CartesianDiagramDataCompressor::CachePosition CartesianDiagramDataCompressor::mapToCache(
    int row, int column) const
{
    Q_ASSERT(m_datasetDimension != 0);

    if (m_data.size() == 0 || m_data[0].size() == 0) {
        return mapToCache(QModelIndex());
    }
    // assumption: indexes per column == 1
    if (indexesPerPixel() == 0) {
        return mapToCache(QModelIndex());
    }
    return CachePosition(int(row / indexesPerPixel()), column / m_datasetDimension);
}

QModelIndexList CartesianDiagramDataCompressor::mapToModel(const CachePosition &position) const
{
    QModelIndexList indexes;
    if (!mapsToModelIndex(position)) {
        return indexes;
    }

    Q_ASSERT(position.column < modelDataColumns());
    if (m_datasetDimension == 2) {
        indexes << m_model->index(position.row, position.column * 2, m_rootIndex); // checked
        indexes << m_model->index(position.row, position.column * 2 + 1, m_rootIndex); // checked
    } else {
        // here, indexes per column is usually but not always 1 (e.g. stock diagrams can have three
        // or four dimensions: High-Low-Close or Open-High-Low-Close)
        const qreal ipp = indexesPerPixel();
        const int baseRow = floor(position.row * ipp);
        // the following line needs to work for the last row(s), too...
        const int endRow = floor((position.row + 1) * ipp);
        for (int row = baseRow; row < endRow; ++row) {
            Q_ASSERT(row < m_model->rowCount(m_rootIndex));
            const QModelIndex index = m_model->index(row, position.column, m_rootIndex);
            if (index.isValid()) {
                indexes << index;
            }
        }
    }
    return indexes;
}

qreal CartesianDiagramDataCompressor::indexesPerPixel() const
{
    if (!m_model || m_data.size() == 0 || m_data[0].size() == 0) {
        return 0;
    }
    return qreal(m_model->rowCount(m_rootIndex)) / qreal(m_data[0].size());
}

bool CartesianDiagramDataCompressor::mapsToModelIndex(const CachePosition &position) const
{
    return m_model && m_data.size() > 0 && m_data[0].size() > 0 && position.column >= 0 && position.column < m_data.size() && position.row >= 0 && position.row < m_data[0].size();
}

void CartesianDiagramDataCompressor::invalidate(const CachePosition &position)
{
    if (mapsToModelIndex(position)) {
        m_data[position.column][position.row] = DataPoint();
        // Also invalidate the data value attributes at "position".
        // Otherwise the user overwrites the attributes without us noticing
        // it because we keep reading what's in the cache.
        m_dataValueAttributesCache.remove(position);
    }
}

bool CartesianDiagramDataCompressor::isCached(const CachePosition &position) const
{
    Q_ASSERT(mapsToModelIndex(position));
    const DataPoint &p = m_data[position.column][position.row];
    return p.index.isValid();
}

void CartesianDiagramDataCompressor::calculateSampleStepWidth()
{
    if (m_mode == Precise) {
        m_sampleStep = 1;
        return;
    }

    static const unsigned int SomePrimes[] = {
        2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
        53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101,
        151, 211, 313, 401, 503, 607, 701, 811, 911, 1009,
        10037, 12911, 16001, 20011, 50021,
        100003, 137867, 199999, 500009, 707753, 1000003, 0}; // ... after that, having a model at all becomes impractical

    // we want at least 17 samples per data point, using a prime step width
    const qreal WantedSamples = 17;
    if (WantedSamples > indexesPerPixel()) {
        m_sampleStep = 1;
    } else {
        int i;
        for (i = 0; SomePrimes[i] != 0; ++i) {
            if (WantedSamples * SomePrimes[i + 1] > indexesPerPixel()) {
                break;
            }
        }
        m_sampleStep = SomePrimes[i];
        if (SomePrimes[i] == 0) {
            m_sampleStep = SomePrimes[i - 1];
        } else {
            m_sampleStep = SomePrimes[i];
        }
    }
}

void CartesianDiagramDataCompressor::setDatasetDimension(int dimension)
{
    if (dimension != m_datasetDimension) {
        m_datasetDimension = dimension;
        rebuildCache();
        calculateSampleStepWidth();
    }
}
